﻿/*****************************************************************
 * Copyright 2010 Joel Parrish
 *****************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Linq;
using System.Xml.Linq;
using InTheHand.Net.Bluetooth;
using InTheHand.Net.Sockets;
using InTheHand.Net;
using Brecham.Obex;
using Brecham.Obex.Objects;

namespace NuiBlue.Bluetooth
{
    /// <summary>
    /// Transfer image files over bluetooth. (Multi-Touch Ready)
    /// </summary>
    public class BluetoothController
    {
        /// <summary>
        /// delegate for handling folder browsing event
        /// </summary>
        /// <param name="sender">object that fired event</param>
        /// <param name="folder">current folder</param>
        public delegate void FolderHandler(object sender, BluetoothEventArgs e);
        /// <summary>
        /// delegate for handling image downloaded event
        /// </summary>
        /// <param name="sender">object that fired the event</param>
        /// <param name="e">holds information of file transfered</param>
        public delegate void DataDownloadedHandler(object sender, BluetoothEventArgs e);
        /// <summary>
        /// delegate for handling image uploaded event
        /// </summary>
        /// <param name="sender">object that fired the event</param>
        /// <param name="device">device address that uploaded an image</param>
        public delegate void DataUploadedHandler(object sender, string device);
        /// <summary>
        /// delegate for handling status updates
        /// </summary>
        /// <param name="sender">object that fired the event</param>
        /// <param name="status">status updates</param>
        public delegate void StatusEventHandler(object sender, string status);

        /// <summary>
        /// event that is fired when a folder is navigated to.
        /// </summary>
        public event FolderHandler navigated;
        /// <summary>
        /// event that is fired when image is downloaded
        /// </summary>
        public event DataDownloadedHandler dataDownloaded;
        /// <summary>
        /// event that is fired when image is uploaded
        /// </summary>
        public event DataUploadedHandler dataUploaded;
        /// <summary>
        /// event that is fired when a status change has occured
        /// </summary>
        public event StatusEventHandler StatusUpdate;

        public List<string> navigatedFolders { get; private set; }

        //picture folder
        private Environment.SpecialFolder specialFolder = Environment.SpecialFolder.MyDocuments;

        //search folders (auto folder searching)
        private List<string> imageFolders;
        private List<string> sendFile;

        //save location
        private string storagePath { get; set; }

        //Bluetooth
        private BluetoothAddress address;

        //pin
        private string pin;
        private int maxFiles = 0;

        //transfer type
        public enum Type
        {
            image = 0,
            video = 1,
            text = 2
        }

        /// <summary>
        /// Handles transfering of images to and from bluetooth devices.
        /// </summary>
        public BluetoothController()
        {
            navigatedFolders = new List<string>();
            imageFolders = new List<string>();
            sendFile = new List<string>();

            try
            {
                XDocument folderXml = XDocument.Load("./folders.xml");
                
                var folders = from folder in folderXml.Descendants("imagefolder")
                              select folder.Value;

                foreach (var folder in folders)
                {
                    imageFolders.Add(folder);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        /// <summary>
        /// Set the address to connect to
        /// </summary>
        /// <param name="address">device address</param>
        public void setAddress(string address)
        {
            this.address = BluetoothAddress.Parse(address);
        }

        public void setPin(string pin)
        {
            this.pin = pin;
        }

        /// <summary>
        /// Set the max number images that quick image downloads.
        /// </summary>
        /// <param name="max">maximum amount</param>
        public void setMaxFiles(int max)
        {
            maxFiles = max;
        }

        /// <summary>
        /// Upload an data.
        /// 
        /// Supports synch and async uploading.
        /// </summary>
        /// <param name="async">asynchronous transfer</param>
        public void uploadData(bool async, Type type, string folder, params string[] files)
        {
            if (address != null)
            {
                TransferThreadParams tParams = new TransferThreadParams() { type = type, folder = folder, files = files };
                if (async)
                {
                    Thread th = new Thread(uploadData);
                    th.IsBackground = true;
                    th.Start(tParams);
                }
                else
                {
                    uploadData(tParams);
                }
            }
            else
            {
                StatusUpdate(this, "address not set");
            }
        }

        /// <summary>
        /// Download an image.
        /// 
        /// Supports synch and async uploading.
        /// </summary>
        /// <param name="async">asynchronous transfer</param>
        public void downloadData(bool async, Type type, string folder, string file)
        {
            if (address != null)
            {
                //create storage location for transfers
                storagePath = Environment.GetFolderPath(specialFolder);
                storagePath += "\\bluetoothData\\" + DateTime.Today.ToShortDateString();

                //make sure directory is setup to recieve files
                if (!Directory.Exists(storagePath))
                {
                    DirectoryInfo di = Directory.CreateDirectory(storagePath);
                }

                TransferThreadParams tParams = new TransferThreadParams() { type = type, folder = folder, file = file };

                if (async)
                {
                    Thread th = new Thread(downloadData);
                    th.IsBackground = true;
                    th.Start(tParams);
                }
                else
                {
                    downloadData(tParams);
                }
            }
            else
            {
                StatusUpdate(this, "address not set");
            }
        }
        
        /// <summary>
        /// Reset the the navigated folder to the root directory of the remote device.
        /// </summary>
        /// <remarks>folder methods are used as reference paths when downloading or uploading</remarks>
        /// <param name="async"></param>
        public void resetFolder(bool async)
        {
            navigatedFolders.Clear();
            TransferThreadParams tParams = new TransferThreadParams { navigatedFolders = new List<string>() };

            if (async)
            {
                Thread th = new Thread(resetFolder);
                th.IsBackground = true;
                th.Start(tParams);
            }
            else
            {
                resetFolder(tParams);
            }
        }

        /// <summary>
        /// Get the folders from the current directory of the device.
        /// </summary>
        /// <remarks>folder methods are used as reference paths when downloading or uploading</remarks>
        /// <param name="async"></param>
        public void getFolders(bool async)
        {
            if (async)
            {
                Thread th = new Thread(getFolders);
                th.IsBackground = true;
                th.Start(null);
            }
            else
            {
                getFolders(null);
            }
        }

        /// <summary>
        /// Navigate to the selected folder and store path in navigatedFolder variable
        /// </summary>
        /// <remarks>folder methods are used as reference paths when downloading or uploading</remarks>
        /// <param name="async"></param>
        /// <param name="folder">folder to select</param>
        public void selectFolder(bool async, string folder)
        {
            TransferThreadParams tParams = new TransferThreadParams { navigatedFolders = new List<string>(navigatedFolders), folder = folder };
            navigatedFolders.Add(folder);
            
            if (async)
            {
                Thread th = new Thread(selectFolder);
                th.IsBackground = true;
                th.Start(tParams);
            }
            else
            {
                selectFolder(tParams);
            }
        }

        /// <summary>
        /// Moves back one directory from the navigated folder.
        /// </summary>
        /// <remarks>folder methods are used as reference paths when downloading or uploading</remarks>
        /// <param name="async"></param>
        public void folderUp(bool async)
        {
            if (navigatedFolders.Count > 0)
                navigatedFolders.RemoveAt(navigatedFolders.Count - 1);

            TransferThreadParams tParams = new TransferThreadParams { navigatedFolders = new List<string>(navigatedFolders) };

            if (async)
            {
                Thread th = new Thread(folderUp);
                th.IsBackground = true;
                th.Start(tParams);
            }
            else
            {
                folderUp(tParams);
            }
        }

        #region workers
        /// <summary>
        /// Method that can be run from a seperate thread to send data to a
        /// target device.
        /// </summary>
        internal void uploadData(object tParams)
        {
            TransferThreadParams data = (TransferThreadParams)tParams;
            SessionHolder sessionHolder = new SessionHolder(address, pin);

            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                ObexClientSession session = sessionHolder.session;

                if (data.files.Length > 0)
                {
                    List<string> dataFiles = new List<string>();

                    if (data.folder == null)
                    {
                        findFolder(session, data.type);
                    }

                    try
                    {
                        int count = 1;
                        foreach (string file in data.files)
                        {
                            StatusUpdate(this, "Sending..." + count + " of " + data.files.Length);
                            session.PutFile(file);

                            count++;
                        }

                        StatusUpdate(this, "done");
                        dataUploaded(this, this.address.ToString());
                    }
                    catch (IOException io)
                    {
                        StatusUpdate(this, "Connection Error Occured");
                    }
                    catch (Exception ex)
                    {
                        StatusUpdate(this, "Error Occured");
                    }
                }
                else
                {
                    StatusUpdate(this, "No Images Selected");
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }

        /// <summary>
        /// Method that can be run from a seperate thread to pull the most recent 
        /// images taken on the target device.
        /// </summary>
        internal void downloadData(object tParams)
        {
            TransferThreadParams data = (TransferThreadParams)tParams;

            SessionHolder sessionHolder = new SessionHolder(address, pin);
            ObexClientSession session;

            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                session = sessionHolder.session;
            }

            ObexFolderListing obexFolderList = null;
            List<ObexFileItem> dataFiles = new List<ObexFileItem>();

            if (data.folder == null)
            {
                obexFolderList = findFolder(session, data.type);
            }

            if (obexFolderList != null)
            {
                foreach (ObexFileItem file in obexFolderList.Files)
                {
                    if (data.files != null)
                    {
                        foreach (string f in data.files)
                        {
                            if (f.Equals(file.Name))
                            {
                                switch (data.type)
                                {
                                    case Type.image:
                                        if (Path.GetExtension(file.Name).Equals(".jpg", StringComparison.CurrentCultureIgnoreCase) ||
                                            Path.GetExtension(file.Name).Equals(".bmp", StringComparison.CurrentCultureIgnoreCase) ||
                                            Path.GetExtension(file.Name).Equals(".png", StringComparison.CurrentCultureIgnoreCase))
                                        {
                                            dataFiles.Add(file);
                                        }
                                        break;
                                }
                                
                            }
                        }
                    }
                    else if (data.file != null)
                    {
                        if (data.file.Equals(file.Name))
                        {
                            switch (data.type)
                            {
                                case Type.image:
                                    if (Path.GetExtension(file.Name).Equals(".jpg", StringComparison.CurrentCultureIgnoreCase) ||
                                        Path.GetExtension(file.Name).Equals(".bmp", StringComparison.CurrentCultureIgnoreCase) ||
                                        Path.GetExtension(file.Name).Equals(".png", StringComparison.CurrentCultureIgnoreCase))
                                    {
                                        dataFiles.Add(file);
                                    }
                                    break;
                            }
                        }

                        break;
                    }
                    else
                    {
                        switch (data.type)
                                {
                                    case Type.image:
                                        if (Path.GetExtension(file.Name).Equals(".jpg", StringComparison.CurrentCultureIgnoreCase) ||
                                            Path.GetExtension(file.Name).Equals(".bmp", StringComparison.CurrentCultureIgnoreCase) ||
                                            Path.GetExtension(file.Name).Equals(".png", StringComparison.CurrentCultureIgnoreCase))
                                        {
                                            dataFiles.Add(file);
                                        }
                                        break;
                                }
                    }
                }

                //put the most recent files first
                if (dataFiles.Count > 0)
                {
                   //dataFiles.Sort();
                    dataFiles.Reverse();
                }

                int max = 0;
                foreach (ObexFileItem fileItem in dataFiles)
                {
                    //full path
                    string fileName;

                    FileStream hostStream;

                    //create stream to grab file from device
                    try
                    {
                        fileName = System.IO.Path.Combine(storagePath, fileItem.Name);

                        hostStream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.None);
                    }
                    catch (System.IO.IOException ex)
                    {
                        Random rnd = new Random();
                        fileName = System.IO.Path.Combine(storagePath, rnd.Next(1, 2345).ToString() + fileItem.Name);

                        hostStream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.None);
                    }

                    StatusUpdate(this, "Transfering");

                    try
                    {
                        //transfer file from device
                        session.GetTo(hostStream, fileItem.Name, null);
                        StatusUpdate(this, "Transfered");
                    }
                    catch (Exception e)
                    {
                        StatusUpdate(this, "Connection Error Occured");
                    }

                    BluetoothEventArgs arge = new BluetoothEventArgs(address.ToString(), fileName, fileItem.Name);
                    dataDownloaded(this, arge);

                    max++;
                    if (max == maxFiles)
                    {
                        break;
                    }
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }

        /// <summary>
        /// Reset the the navigated folder to the root directory of the remote device.
        /// </summary>
        /// <param name="sessionHolder"></param>
        internal void resetFolder(object tParams)
        {
            SessionHolder sessionHolder = null;
            TransferThreadParams data = null;

            if (tParams != null)
            {
                data = (TransferThreadParams)tParams;
                sessionHolder = data.session;
            }

            if (sessionHolder == null || sessionHolder.session == null)
            {
                sessionHolder = new SessionHolder(address, pin);
            }

            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                ObexClientSession session = sessionHolder.session;

                ObexFolderListing obexFolderList = null;
                List<ObexFileItem> dataFiles = new List<ObexFileItem>();

                try
                {
                    sessionHolder.session.SetPathReset();

                    //getfolders in current directory
                    obexFolderList = session.GetFolderListing();

                    //extract the list of folders
                    IList<ObexFolderItem> folderList = obexFolderList.Folders;
                    IList<ObexFileItem> fileList = obexFolderList.Files;

                    string[] folders = new string[folderList.Count];
                    string[] files = new string[fileList.Count];

                    for (int i = 0; i < folderList.Count; i++)
                    {
                        folders[i] = folderList[i].Name;
                    }

                    for (int i = 0; i < fileList.Count; i++)
                    {
                        files[i] = fileList[i].Name;
                    }

                    BluetoothEventArgs ev = new BluetoothEventArgs(address.ToString(), null, null);
                    ev.setFolders(folders);
                    ev.setFiles(files);

                    navigated(this, ev);
                }
                catch (Exception e)
                {
                    StatusUpdate(this, "Error occured while resetting folders");
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }

        /// <summary>
        /// Get the folders from the current directory of the device.
        /// </summary>
        /// <param name="sessionHolder"></param>
        internal void getFolders(object tParams)
        {
            SessionHolder sessionHolder = null;

            if(tParams != null)
                sessionHolder = ((TransferThreadParams)tParams).session;

            if (sessionHolder == null || sessionHolder.session == null)
            {
                sessionHolder = new SessionHolder(address, pin);
            }

            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                ObexClientSession session = sessionHolder.session;

                ObexFolderListing obexFolderList = null;
                List<ObexFileItem> dataFiles = new List<ObexFileItem>();

                try
                {
                    //getfolders in current directory
                    obexFolderList = session.GetFolderListing();

                    //extract the list of folders
                    IList<ObexFolderItem> folderList = obexFolderList.Folders;
                    IList<ObexFileItem> fileList = obexFolderList.Files;

                    string[] folders = new string[folderList.Count];
                    string[] files = new string[fileList.Count];

                    for (int i = 0; i < folderList.Count; i++)
                    {
                        folders[i] = folderList[i].Name;
                    }

                    for (int i = 0; i < fileList.Count; i++)
                    {
                        files[i] = fileList[i].Name;
                    }

                    BluetoothEventArgs ev = new BluetoothEventArgs(address.ToString(), null, null);
                    ev.setFolders(folders);
                    ev.setFiles(files);

                    navigated(this, ev);
                }
                catch (Exception e)
                {
                    StatusUpdate(this, "Error occured while getting folders");
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }

        /// <summary>
        /// Navigate to the selected folder and store path in navigatedFolder variable
        /// </summary>
        /// <param name="tParams"></param>
        internal void selectFolder(object tParams)
        {
            TransferThreadParams data = (TransferThreadParams)tParams;

            SessionHolder sessionHolder = new SessionHolder(address, pin);
            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                ObexClientSession session = sessionHolder.session;

                ObexFolderListing obexFolderList = null;
                List<ObexFileItem> dataFiles = new List<ObexFileItem>();

                try
                {
                    foreach (string folder in data.navigatedFolders)
                    {
                        session.SetPath(folder);
                    }
                    session.SetPath(data.folder);
                    
                    //getfolders in current directory
                    obexFolderList = session.GetFolderListing();

                    //extract the list of folders
                    IList<ObexFolderItem> folderList = obexFolderList.Folders;
                    IList<ObexFileItem> fileList = obexFolderList.Files;

                    string[] folders = new string[folderList.Count];
                    string[] files = new string[fileList.Count];

                    for (int i = 0; i < folderList.Count; i++)
                    {
                        folders[i] = folderList[i].Name;
                    }

                    for (int i = 0; i < fileList.Count; i++)
                    {
                        files[i] = fileList[i].Name;
                    }

                    BluetoothEventArgs ev = new BluetoothEventArgs(address.ToString(), null, null);
                    ev.setFolders(folders);
                    ev.setFiles(files);

                    navigated(this, ev);
                }
                catch (Exception e)
                {
                    StatusUpdate(this, "Error occured while selecting folder");
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }

        /// <summary>
        /// Moves back one directory from the navigated folder.
        /// </summary>
        /// <param name="sessionHolder"></param>
        internal void folderUp(object tParams)
        {
            SessionHolder sessionHolder = null;
            TransferThreadParams data = null;

            if (tParams != null)
            {
                data = (TransferThreadParams)tParams;
                sessionHolder = data.session;
            }

            if (sessionHolder == null || sessionHolder.session == null)
            {
                sessionHolder = new SessionHolder(address, pin);
            }

            if (sessionHolder.session == null)
            {
                StatusUpdate(this, "Error connecting");
                return;
            }
            else
            {
                ObexClientSession session = sessionHolder.session;

                ObexFolderListing obexFolderList = null;
                List<ObexFileItem> dataFiles = new List<ObexFileItem>();

                try
                {
                    foreach (string folder in data.navigatedFolders)
                        sessionHolder.session.SetPath(folder);

                    //getfolders in current directory
                    obexFolderList = session.GetFolderListing();

                    //extract the list of folders
                    IList<ObexFolderItem> folderList = obexFolderList.Folders;
                    IList<ObexFileItem> fileList = obexFolderList.Files;

                    string[] folders = new string[folderList.Count];
                    string[] files = new string[fileList.Count];

                    for (int i = 0; i < folderList.Count; i++)
                    {
                        folders[i] = folderList[i].Name;
                    }

                    for (int i = 0; i < fileList.Count; i++)
                    {
                        files[i] = fileList[i].Name;
                    }

                    BluetoothEventArgs ev = new BluetoothEventArgs(address.ToString(), null, null);
                    ev.setFolders(folders);
                    ev.setFiles(files);

                    navigated(this, ev);
                }
                catch (Exception e)
                {
                    StatusUpdate(this, "Error occured while moving up a folder");
                }
            }

            if (sessionHolder != null)
                sessionHolder.closeConnection();
        }
        #endregion

        private ObexFolderListing findFolder(ObexClientSession session, Type type)
        {
            bool continueSearching = false;
            string nextFolder = null;

            ObexFolderListing obexFolderList = null;
            LinkedList<string> folders;

            switch (type)
            {
                case Type.image:
                    folders = new LinkedList<string>(imageFolders);
                    break;
                default:
                    folders = new LinkedList<string>();
                    break;
            }

            if (folders.Count == 0)
            {
                StatusUpdate(this, "No folders found (make sure folders.xml is in application folder)");
                return null;
            }

            do
            {
                continueSearching = false;

                //getfolders in current directory
                obexFolderList = session.GetFolderListing();

                //extract the list of folders
                IList<ObexFolderItem> folderList = obexFolderList.Folders;

                ObexFolderItem dataFolder = null;

                //search through folders
                foreach (ObexFolderItem folder in folderList)
                {
                    if (folders.Contains(folder.Name.ToUpper()))
                    {
                        nextFolder = folder.Name;

                        dataFolder = folder;
                        continueSearching = true;
                    }
                }

                if (nextFolder != null)
                {
                    session.SetPath(nextFolder);
                    nextFolder = null;
                }

                //continue searching if there might be deeper folders
            } while (continueSearching);

            return obexFolderList;
        }

        /// <summary>
        /// Remove device from registry (used for proximity sensing)
        /// </summary>
        public void removeDevice()
        {
            BluetoothSecurity.RemoveDevice(address);
        }
    }
}
